﻿// <copyright file="MapEntry.cs" company="DataAbstractor">
//   (c) Luke Geraghty 2013
// </copyright>
// <summary>
//   Mapping between two unrelated classes.
// </summary>

namespace DataAbstractor
{
    using System;
    using System.Collections;
    using System.Collections.Generic;
    using System.Linq;
    using System.Reflection;

    /// <summary>
    /// Mapping between two unrelated classes.
    /// </summary>
    internal class MapEntry
    {
        /// <summary>
        /// The properties and the destination class writable properties corresponding to source values.
        /// </summary>
        private readonly Dictionary<PropertyInfo, MapValue> mapping;

        /// <summary>
        /// The source properties.
        /// </summary>
        private readonly PropertyInfo[] sourceClassGetters;

        /// <summary>
        /// Initializes a new instance of the <see cref="MapEntry"/> class.
        /// </summary>
        /// <param name="destination">The destination.</param>
        /// <param name="source">The source.</param>
        public MapEntry(Type destination, Type source)
        {
            this.Destination = destination;
            this.Source = source;

            var setProperties = this.Destination.GetProperties(BindingFlags.Public | BindingFlags.Instance).Where(i => i.CanWrite);
            this.sourceClassGetters = this.Source.GetProperties(BindingFlags.Public | BindingFlags.Instance).Where(i => i.CanRead).ToArray();
            this.mapping = setProperties.ToDictionary(
                i => i,
                i =>
                {
                    var getter = this.sourceClassGetters.FirstOrDefault(j => j.Name == i.Name);
                    if (getter == null || (!i.PropertyType.IsClass && getter.PropertyType != i.PropertyType))
                    {
                        return null;
                    }

                    return new MapValue { ToClassMember = getter };
                });
        }

        /// <summary>
        /// Gets the destination type.
        /// </summary>
        /// <value>
        /// The destination type.
        /// </value>
        public Type Destination { get; private set; }

        /// <summary>
        /// Gets the source type.
        /// </summary>
        /// <value>
        /// The source type.
        /// </value>
        public Type Source { get; private set; }

        /// <summary>
        /// Gets or sets the abstraction complete routine.
        /// </summary>
        /// <value>
        /// The abstraction complete routine.
        /// </value>
        public Action<object, object> AbstractionComplete { get; set; }

        /// <summary>
        /// Gets a value indicating whether the mapping is complete.
        /// </summary>
        /// <value>
        /// The map is complete.
        /// </value>
        public bool IsFullyMapped
        {
            get
            {
                return !this.FieldsNotMapped.Any();
            }
        }

        /// <summary>
        /// Gets the fields not mapped.
        /// </summary>
        /// <value>
        /// The fields not mapped.
        /// </value>
        public IEnumerable<string> FieldsNotMapped
        {
            get
            {
                return this.mapping.Where(i => i.Value == null).Select(i => i.Key.Name);
            }
        }

        /// <summary>
        /// Perform the mapping.
        /// </summary>
        /// <param name="destination">The destination class.</param>
        /// <param name="source">The source class.</param>
        /// <param name="deepCopy">If true copy sub-classes.</param>
        public void Perform(object destination, object source, bool deepCopy)
        {
            foreach (var mapValue in this.mapping.Where(i => i.Value != null && !i.Value.Ignore))
            {
                var value = mapValue.Value.ToClassMember != null
                                   ? mapValue.Value.ToClassMember.GetValue(source, null)
                                   : mapValue.Value.Value;
                if (value != null)
                {
                    var valueType = value.GetType();
                    if (valueType.IsClass && valueType != typeof(string) && deepCopy)
                    {
                        value = DataAbstractor.To(mapValue.Key.PropertyType, value, true);
                    }
                }

                mapValue.Key.SetValue(destination, value, null);
            }

            var abstractionComplete = this.AbstractionComplete;
            if (abstractionComplete != null)
            {
                abstractionComplete(destination, source);
            }
        }

        /// <summary>
        /// Set map between destination property and map value.
        /// </summary>
        /// <param name="destination">The name of the destination field.</param>
        /// <param name="value">The value to place in the field.</param>
        /// <returns>True if successful.</returns>
        public bool SetMap(string destination, MapValue value)
        {
            var entry = this.mapping.FirstOrDefault(i => i.Key.Name == destination);
            if (entry.Equals(default(KeyValuePair<PropertyInfo, MapValue>)))
            {
                return false;
            }

            this.mapping[entry.Key] = value;
            return true;
        }

        /// <summary>
        /// The value of the map.
        /// </summary>
        internal class MapValue
        {
            /// <summary>
            /// Gets or sets the value.
            /// </summary>
            /// <value>
            /// The value.
            /// </value>
            public object Value { get; set; }

            /// <summary>
            /// Gets or sets the to class member.
            /// </summary>
            /// <value>
            /// The to class member.
            /// </value>
            public PropertyInfo ToClassMember { get; set; }

            /// <summary>
            /// Gets or sets a value indicating whether a member is to be ignored.
            /// </summary>
            /// <value>
            /// The ignore member.
            /// </value>
            public bool Ignore { get; set; }
        }
    }
}
